package org.apache.ibatis.reflection;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class ParamNameResolver {

    private static final String GENERIC_NAME_PREFIX = "param";

    /**
     * <p>
     * The key is the index and the value is the name of the parameter.<br />
     * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
     * the parameter index is used. Note that this index could be different from the actual index
     * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
     * </p>
     * <ul>
     * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
     * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
     * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
     * </ul>
     * <p>
     * 记录了参数在参数列表中的位置索引与参数名称之间的对应关系，其中key表示参数在参数列表中的索引位置，
     * value表示参数名称，参数名称可以通过＠Param注解指定，如果没有指定＠Param注解，则使用参数索引作为其名称
     */
    private final SortedMap<Integer, String> names;

    /**
     * 记录对应方法的参数列表中是否使用了＠Param 注解
     */
    private boolean hasParamAnnotation;

    public ParamNameResolver(Configuration config, Method method) {
        // 获取参数列表中每个参数的类型
        final Class<?>[] paramTypes = method.getParameterTypes();
        // 获取参数列表上的注解
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        // 该集合用于记录参数索引与参数名称的对应关系
        final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
        int paramCount = paramAnnotations.length;
        // get names from @Param annotations
        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
            if (isSpecialParameter(paramTypes[paramIndex])) {
                // skip special parameters
                // 如果参数是RowBounds类型或ResultHandler类型，则跳过对该参数的分析
                continue;
            }
            String name = null;
            // 遍历该参数对应的注解集合
            for (Annotation annotation : paramAnnotations[paramIndex]) {
                if (annotation instanceof Param) {
                    // @Param注解出现过一次，就将hasParamAnnotation初始化为true
                    hasParamAnnotation = true;
                    // 获取自Param注解指定的参数名称
                    // 获取 @Param 注解内容
                    name = ((Param) annotation).value();
                    break;
                }
            }
            // name 为空，表明未给参数配置 @Param 注解
            // 这个if代码段解释了上面的示例中names集合项的value为什么是"0"和"1"
            if (name == null) {
                // @Param was not specified.
                // 该参数没有对应的＠Param注解，则根据配置决定是否使用参数实际名称作为其名称
                // 检测是否设置了 useActualParamName 全局配置
                if (config.isUseActualParamName()) {
                    /*
                     * 通过反射获取参数名称。此种方式要求 JDK 版本为 1.8+，
                     * 且要求编译时加入 -parameters 参数，否则获取到的参数名
                     * 仍然是 arg1, arg2, ..., argN
                     */
                    name = getActualParamName(method, paramIndex);
                }
                if (name == null) {
                    /*
                     * 使用参数的索引作为其名称, use the parameter index as the name ("0", "1", ...),
                     * 使用 map.size() 返回值作为名称，思考一下为什么不这样写： name = String.valueOf(paramIndex);
                     * 因为如果参数列表中包含 RowBounds 或 ResultHandler，这两个参数会被忽略掉，这样将导致名称不连续。
                     * 比如参数列表 (int p1, int p2, RowBounds rb, int p3),期望得到名称列表为 ["0", "1", "2"], 实际得到名称列表为 ["0", "1", "3"]
                     */
                    name = String.valueOf(map.size());
                }
            }
            // 记录到map中保存
            // 存储 paramIndex 到 name 的映射
            map.put(paramIndex, name);
        }
        // 初始化names集合
        // 参数列表详细Map
        names = Collections.unmodifiableSortedMap(map);
    }

    private String getActualParamName(Method method, int paramIndex) {
        if (Jdk.parameterExists) {
            return ParamNameUtil.getParamNames(method).get(paramIndex);
        }
        return null;
    }

    /**
     * isSpecialParameter()方法用来过滤RowBounds和ResultHandler两种类型的参数
     *
     * @param clazz
     * @return
     */
    private static boolean isSpecialParameter(Class<?> clazz) {
        return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
    }

    /**
     * Returns parameter names referenced by SQL providers.
     */
    public String[] getNames() {
        return names.values().toArray(new String[0]);
    }

    /**
     * <p>
     * A single non-special parameter is returned without a name.<br />
     * Multiple parameters are named using the naming rule.<br />
     * In addition to the default names, this method also adds the generic names (param1, param2,
     * ...).
     * </p>
     * 该方法接收的参数是用户传入的实参列表，并将实参与其对应名称进行关联
     */
    public Object getNamedParams(Object[] args) {
        final int paramCount = names.size();
        if (args == null || paramCount == 0) {
            // 无参数, 返回null
            return null;
        } else if (!hasParamAnnotation && paramCount == 1) {
            // 未使用自Param且只有一个参数
            /*
             * 如果方法参数列表无 @Param 注解，且仅有一个非特别参数，则返回该参数的值。
             * 比如如下方法：
             *     List findList(RowBounds rb, String name)
             * names 如下：
             *     names = {1 : "0"}
             * 此种情况下，返回 args[names.firstKey()]，即 args[1] -> name
             */
            return args[names.firstKey()];
        } else { // 处理使用@Param注解指定了参数名称或有多个参数的情况
            // param这个Map中记录了参数名称与实参之间的对应关系。ParamMap继承了HashMap，如果向
            // ParamMap中添加已经存在的key，会报错，其他行为与HashMap相同
            final Map<String, Object> param = new ParamMap<Object>();
            int i = 0;
            for (Map.Entry<Integer, String> entry : names.entrySet()) {
                // 将参数名与实参对应关系记录到param中
                // 添加 <参数名, 参数值> 键值对到 param 中
                param.put(entry.getValue(), args[entry.getKey()]);
                // 下面是为参数创建"param＋索号"格式的默认参数名称，例如：paraml, param2等，并添加到param集合中
                // add generic param names (param1, param2, ...)
                // genericParamName = param + index。比如 param1, param2, ... paramN
                final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
                // ensure not to overwrite parameter named with @Param
                // 如采＠Param注解指定的参数名称就是"param＋索引"格式的，则不需要再添加
                /*
                 * 检测 names 中是否包含 genericParamName，什么情况下会包含？答案如下：
                 *
                 *   使用者显式将参数名称配置为 param1，即 @Param("param1")
                 */
                if (!names.containsValue(genericParamName)) {
                    // 添加 <param*, value> 到 param 中
                    param.put(genericParamName, args[entry.getKey()]);
                }
                i++;
            }
            return param;
        }
    }
}
